/*
 * Created on 11 Jul 2008 by Andrew White
 */

package com.volatileshooter.networking.client;

import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.channels.DatagramChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.util.Iterator;

import com.volatileshooter.networking.io.GameChannel;
import com.volatileshooter.networking.io.GamePacket;
import com.volatileshooter.networking.io.packets.AcceptPacket;
import com.volatileshooter.networking.io.packets.ChatPacket;
import com.volatileshooter.networking.io.packets.JoinPacket;
import com.volatileshooter.networking.io.packets.QuitPacket;
import com.volatileshooter.networking.io.packets.RejectPacket;
import com.volatileshooter.networking.server.util.Log;

/*
 * ClientConnection class connects to server and handles client events.
 */
public class ClientConnection {

	/* property objects */
	public String name = "Default Player";

	public int port = 27016;

	public boolean isConnected = false;

	public short id = -1;

	/* nio */
	private DatagramChannel udpChannel;

	private Selector selector;

	/* game channel */
	private GameChannel gameChannel;

	/* client listener */
	private ClientListener listener;

	public ClientConnection() {
		// start logging
		new Log("cyber.client");

		// start server
		Log.info("\nStarting Cyber Client...\n");

		// start logging to file
		String log_path = "client%g[id%u].log";
		try {
			Log.setLogFile(log_path);
		} catch (SecurityException e) {
			Log.warning("Logging to [" + log_path + "] could not begin due to security reasons", e);
		} catch (IOException e) {
			Log.warning("Failed to start logging to [" + log_path + "]", e);
		}
	}

	/**
	 * Initialises client by opening up UDP channel.
	 */
	public void init(SocketAddress server, ClientListener listener) {

		// don't initialise if already connected
		if (udpChannel != null) {
			if (udpChannel.isConnected()) {
				Log.warning("Attempted to initialise whilst connection has already been established", null);
				return;
			}
		}

		// set properties
		isConnected = false;
		id = -1;
		this.listener = listener;

		// log
		Log.info("\n##### Cyber Client #####\n");

		// open UDP channel, in non-blocking mode, bound to local IP and port
		try {
			selector = Selector.open();
			udpChannel = DatagramChannel.open();
			udpChannel.configureBlocking(false);
			udpChannel.socket().bind(new InetSocketAddress(InetAddress.getLocalHost(), port));
			// for security connect channel to server so that only
			// packets from the server are received
			udpChannel.register(selector, SelectionKey.OP_READ);
			udpChannel.connect(server);
		} catch (IOException e) {
			Log.error("Failed to start client", e);
			System.err.println("error: " + e);
		}
		Log.info("Client live on: " + udpChannel.socket().getLocalAddress().getHostAddress() + ":"
				+ udpChannel.socket().getLocalPort());

		// game channel for reading and writing packets
		gameChannel = new GameChannel(udpChannel);

		// send join packet
		JoinPacket join = new JoinPacket(id, null, name);
		gameChannel.sendPacket(join);
	}

	/**
	 * Reads in packets from channel buffer.
	 */
	public void update() {

		// can't update if not connected to server
		if (udpChannel == null)
			return;
		if (!udpChannel.isConnected())
			return;

		try {
			// get selected keys (doesn't block if there is nothing to read)
			selector.select();

			// loop through any keys
			Iterator<SelectionKey> it = selector.selectedKeys().iterator();
			while (it.hasNext()) {
				SelectionKey key = it.next();
				it.remove();

				// check if the key is readable
				if (key.isReadable()) {

					// read packet from channel
					GamePacket packet = gameChannel.readPacket();

					// check if a packet was returned
					if (packet != null) {

						// packet received
						if (packet instanceof AcceptPacket) {

							// accept packet received
							AcceptPacket accept = (AcceptPacket) packet;
							// get ID
							id = accept.clientId;
							// set connection to accepted
							isConnected = true;
							// call accepted event
							listener.accepted(accept.getMessage());

						} else if (packet instanceof RejectPacket) {

							// reject packet received
							RejectPacket reject = (RejectPacket) packet;
							// call rejected event
							listener.rejected(reject.getReason());

						} else if (packet instanceof JoinPacket) {
						    
						    // join packet received
						    JoinPacket join = (JoinPacket) packet;
						    // call join event
						    listener.joined(join.clientId, join.getName());
						    
						} else if (packet instanceof QuitPacket) {

							// quit packet received
							QuitPacket quit = (QuitPacket) packet;
							// call quit event
							listener.quit(quit.clientId);

						} else if (packet instanceof ChatPacket) {

							// chat packet received
							ChatPacket chat = (ChatPacket) packet;
							// call chat event
							listener.chat(chat.clientId, chat.getChat());

						} else {

							// event type received that is not supported
							// by server
							Log.warning("Ignoring packet from " + packet.clientId + " [" + packet.addr
									+ "], unknown event type " + packet.event, null);

						}
					}
				}
			}
		} catch (IOException e) {
			Log.warning("Error receiving packet", e);
		}
	}

	/**
	 * Cleans up by closing connections, to be called before closing the
	 * client.
	 */
	public void cleanup() {

		// send quit packet
		QuitPacket quit = new QuitPacket(id, null);
		gameChannel.sendPacket(quit);

		// close selector and UDP channel
		try {
			selector.close();
			udpChannel.close();
		} catch (IOException e) {
			Log.error("Failed to close UDP channel", e);
		}

	}

	/**
	 * Adds a new ClientListener to listen for client events.
	 * 
	 * @param listener
	 *             Object that can receive client events
	 */
	public void addListener(ClientListener listener) {
		this.listener = listener;
	}

	public void sendChat(String chat) {
		ChatPacket packet = new ChatPacket(id, null, chat);
		gameChannel.sendPacket(packet);
	}
}
